package util

import (
	"archive/tar"
	"bytes"
	"context"
	"errors"
	"fmt"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/client"
	"github.com/docker/docker/pkg/stdcopy"
	"io"
	"io/ioutil"
	"os"
	"path"
	"regexp"
	"strings"
	"time"
)

type DockerCli struct {
	Client *client.Client
	Ctx    context.Context
	Env    []string
}

//开启docker api
//vim  /lib/systemd/system/docker.service
//ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375
//sudo systemctl daemon-reload
//sudo systemctl restart docker
func NewDockerCli() *DockerCli {
	//cli, err := client.NewClient("tcp://127.0.0.1:2375", "v1.39", nil, nil)
	cli, err := client.NewClientWithOpts(client.WithAPIVersionNegotiation())
	if err != nil {
		panic(err)
	}
	ctx := context.Background()
	return &DockerCli{
		Client: cli,
		Ctx:    ctx,
	}
}

func (cli *DockerCli) SetEnv(val []string) {
	cli.Env = val
}

//在docker中运行linux命令 完整返回
func (cli *DockerCli) Run(containerId, cmd string) (string, error) {
	_ = cli.Client.ContainerStart(cli.Ctx, containerId, types.ContainerStartOptions{})
	cmd = cmd + ` || if [ $? -ne 0 ];then echo qmangoliaExitCode：${$?}; fi`
	execConfig := types.ExecConfig{
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"/bin/bash", "-c", cmd},
		Tty:          true,
		AttachStdin:  true,
		Detach:       true,
		Env:          cli.Env,
	}
	cresp, err := cli.Client.ContainerExecCreate(cli.Ctx, containerId, execConfig)
	if err != nil {
		return "", err
	}
	execID := cresp.ID
	aresp, err := cli.Client.ContainerExecAttach(cli.Ctx, execID, types.ExecStartCheck{})
	if err != nil {
		return "", err
	}
	defer aresp.Close()

	var outBuf, errBuf bytes.Buffer
	outputDone := make(chan error)
	go func() {
		_, err = stdcopy.StdCopy(&outBuf, &errBuf, aresp.Reader)
		outputDone <- err
	}()
	Timeout := 3600 * time.Second
	select {
	case <-time.After(Timeout):
		return "", errors.New(fmt.Sprintf("cmd run timeout, cmd [%s], time[%v]", cmd, Timeout))
	case err := <-outputDone:
		if err != nil {
			return "", err
		}
		break
	case <-cli.Ctx.Done():
		return "", cli.Ctx.Err()
	} //如果没有default字句，select将阻塞，直到某个通信可以运行

	stdout, err := ioutil.ReadAll(&outBuf)
	if err != nil {
		return "", err
	}
	stderr, err := ioutil.ReadAll(&errBuf)
	if err != nil {
		return "", err
	}

	if string(stderr) != "" {
		return "", errors.New(string(stderr))
	}

	//获取退出状态
	iresp, err := cli.Client.ContainerExecInspect(cli.Ctx, execID)
	if err != nil {
		return "", err
	}
	if iresp.ExitCode != 0 { //ExitCode不准确
		//return "", errors.New("当前容器状态：ExitCode " + strconv.Itoa(iresp.ExitCode))
	}

	if cli.VerifyMessage(string(stdout)) == false {
		return "", errors.New(string(stdout))
	}

	return string(stdout), nil
}

//在docker中运行linux命令 && 输出流
func (cli *DockerCli) Exec(containerId, cmd string, queryLine QueryLine) (string, error) {
	cmd = cmd + ` || if [ $? -ne 0 ];then echo qmangoliaExitCode：${$?}; fi`
	var lastLine string
	_ = cli.Client.ContainerStart(cli.Ctx, containerId, types.ContainerStartOptions{})
	execConfig := types.ExecConfig{
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"/bin/bash", "-c", cmd},
		Tty:          true,
		AttachStdin:  true,
		Env:          cli.Env,
	}
	cresp, err := cli.Client.ContainerExecCreate(cli.Ctx, containerId, execConfig)
	if err != nil {
		return "", err
	}

	execID := cresp.ID
	aresp, err := cli.Client.ContainerExecAttach(cli.Ctx, execID, types.ExecStartCheck{Tty: true})
	if err != nil {
		return "", err
	}
	//_ = cli.Client.ContainerResize(cli.Ctx, containerId, types.ResizeOptions{Width: 728, Height: 825})
	//_ = cli.Client.ContainerExecResize(cli.Ctx, execID, types.ResizeOptions{Width: 728, Height: 825})

	defer aresp.Close()
	outputDone := make(chan error)
	/*	go func() {
		for {
			b := make([]byte, 4096)
			n, err := aresp.Reader.Read(b)
			if err != nil {
				outputDone <- err
				return
			}
			b = b[:n]
			byteInfo := string(b)
			if cli.VerifyMessage(byteInfo) == false {
				outputDone <- errors.New(byteInfo)
				return
			}
			queryLine(byteInfo)
		}
	}()*/

	go func() {
		for {
			line, isPrefix, err := aresp.Reader.ReadLine()
			if err != nil {
				outputDone <- err
				return
			}
			byteInfo := string(line)
			if !isPrefix {
				if cli.VerifyMessage(byteInfo) == false {
					outputDone <- errors.New(lastLine + " \r\n系统错误： " + byteInfo)
					return
				}
				if byteInfo != "" {
					lastLine = byteInfo
					queryLine(byteInfo)
				}
			}
		}
	}()

	Timeout := 1800 * time.Second
	select {
	case <-time.After(Timeout):
		return "", errors.New(fmt.Sprintf("cmd run timeout, cmd [%s], time[%v]", cmd, Timeout))
	case err := <-outputDone:
		if err != nil && err != io.EOF { //todo 存在命令异常 但是err为正常EOF的现象
			return "", err
		}
		break
	case <-cli.Ctx.Done():
		return "", cli.Ctx.Err()
	}

	//获取退出状态
	/*iresp, err := cli.Client.ContainerExecInspect(cli.Ctx, execID)
	if err != nil {
		return "", err
	}
	if iresp.ExitCode != 0 { //ExitCode不准确
		return "", errors.New("错误：" + lastLine + ", ExitCode " + strconv.Itoa(iresp.ExitCode))
	}*/
	return "", nil
}

//在docker中运行linux命令 && 输出流
func (cli *DockerCli) Exec2(containerId, cmd string, queryLine QueryLine) (string, error) {
	_ = cli.Client.ContainerStart(cli.Ctx, containerId, types.ContainerStartOptions{})
	execConfig := types.ExecConfig{
		AttachStdout: true,
		AttachStderr: true,
		Cmd:          []string{"/bin/bash", "-c", cmd},
		Tty:          true,
		AttachStdin:  true,
		//Detach:       true,
		Env: cli.Env,
	}
	cresp, err := cli.Client.ContainerExecCreate(cli.Ctx, containerId, execConfig)
	if err != nil {
		return "", err
	}
	execID := cresp.ID
	aresp, err := cli.Client.ContainerExecAttach(cli.Ctx, execID, types.ExecStartCheck{})
	if err != nil {
		return "", err
	}

	_ = cli.Client.ContainerResize(cli.Ctx, containerId, types.ResizeOptions{Width: 728, Height: 825})
	_ = cli.Client.ContainerExecResize(cli.Ctx, execID, types.ResizeOptions{Width: 728, Height: 825})

	defer aresp.Close()

	// read the output
	var outBuf, errBuf bytes.Buffer
	outputDone := make(chan error)
	outputDone2 := make(chan error)

	go func() {
		_, err = stdcopy.StdCopy(&outBuf, &errBuf, aresp.Reader)
		outputDone <- err
		outputDone2 <- err
	}()

	//定时读取实时输出流
	go func() {
		outBufLen := 0
		for {
			select {
			case _ = <-outputDone2:
				return
			case <-cli.Ctx.Done():
				return
			default:
				if outBuf.Len() != outBufLen {
					outBufStr := outBuf.Bytes()[outBufLen:]
					outBufLen = outBuf.Len()
					queryLine(string(outBufStr))
				}
			}
			time.Sleep(time.Millisecond * 300)
		}
	}()

	Timeout := 3600 * time.Second
	select {
	case <-time.After(Timeout):
		return "", errors.New(fmt.Sprintf("cmd run timeout, cmd [%s], time[%v]", cmd, Timeout))
	case err := <-outputDone:
		if err != nil {
			return "", err
		}
		break
	case <-cli.Ctx.Done():
		return "", cli.Ctx.Err()
	} //如果没有default字句，select将阻塞，直到某个通信可以运行

	stdout, err := ioutil.ReadAll(&outBuf)
	if err != nil {
		return "", err
	}
	stderr, err := ioutil.ReadAll(&errBuf)
	if err != nil {
		return "", err
	}

	if string(stderr) != "" {
		return "", errors.New(string(stderr))
	}

	//获取退出状态
	iresp, err := cli.Client.ContainerExecInspect(cli.Ctx, execID)
	if err != nil {
		return "", err
	}
	if iresp.ExitCode != 0 { //ExitCode不准确
		//return "", errors.New("当前容器状态：ExitCode " + strconv.Itoa(iresp.ExitCode))
	}

	if cli.VerifyMessage(string(stdout)) == false {
		return "", errors.New("脚本运行失败 : " + string(stdout))
	}

	return string(stdout), nil
}

/*
	从容器中拷贝文件/目录
	srcPath:本机目录(输入)
	desPath:容器中文件目录(输出)
	containerId:容器ID
	excludePaths:需要拷贝排除的目录
*/
func (cli *DockerCli) CopyFromDocker(srcPath, dstPath, containerId string, excludePaths []string, QueryLine func(line string)) error {
	if err := PathExists(srcPath); err != nil {
		err := os.MkdirAll(srcPath, 755)
		if err != nil {
			return err
		}
	}
	ctx := context.Background()
	rc, _, err := cli.Client.CopyFromContainer(ctx, containerId, dstPath)
	if err != nil {
		return err
	}

	tr := tar.NewReader(rc)
	for hdr, err := tr.Next(); err != io.EOF; hdr, err = tr.Next() {
		if err != nil {
			continue
		}
		fi := hdr.FileInfo()
		fileInfo := fi.Sys().(*tar.Header)
		absPath := fileInfo.Name[strings.Index(fileInfo.Name, "/")+1:] //当前目录 去除project_xxx/
		if fi.IsDir() {
			//排除忽略文件夹
			isExcludePath := false
			for _, excludePath := range excludePaths {
				if len(absPath) >= len(excludePath) {
					if absPath == excludePath { //demo  .git/ == .git/
						isExcludePath = true
						break
					}
					if absPath[:len(excludePath)] == excludePath {
						isExcludePath = true
						break
					}
				}
			}
			//排除忽略文件夹
			if isExcludePath {
				continue
			}
			if strings.Count(fileInfo.Name, "/") <= 2 {
				QueryLine("create path " + fileInfo.Name)
			}
			err = os.MkdirAll(srcPath+"/"+fileInfo.Name, fi.Mode().Perm())
			if err != nil {
				return err
			}
			continue
		}

		//排除忽略文件
		isExcludeFile := false
		for _, excludePath := range excludePaths {
			if len(absPath) >= len(excludePath) {
				if absPath[:len(excludePath)] == excludePath {
					isExcludeFile = true
					break
				}
			}
		}
		if isExcludeFile {
			continue
		}
		//排除忽略文件夹

		fw, err := os.Create(srcPath + "/" + fileInfo.Name)
		if err != nil {
			return err
		}

		if _, err := io.Copy(fw, tr); err != nil {
			return err
		}
		_ = os.Chmod(srcPath+"/"+fileInfo.Name, fi.Mode().Perm())
		_ = fw.Close()
	}
	err = rc.Close()
	if err != nil {
		return err
	}
	return nil
}

/*
	拷贝文件/目录 到容器中
	srcPath:本机目录(输入)
	dstPath:容器中文件目录(输出)
	containerId:容器ID
*/
func (cli *DockerCli) CopyToDocker(srcPath, dstPath, containerId string) (err error) {
	_, err = cli.Run(containerId, "mkdir -p "+dstPath)
	if err != nil {
		return err
	}
	pwdDir, _ := os.Getwd()
	srcPath = pwdDir + srcPath
	// 清理路径字符串
	srcPath = path.Clean(srcPath)
	// 判断要打包的文件或目录是否存在
	if !Exists(srcPath) {
		return errors.New("要拷贝的文件或目录不存在：" + srcPath)
	}

	var buf bytes.Buffer
	tw := tar.NewWriter(&buf)
	defer func() {
		// 这里要判断 tw 是否关闭成功，如果关闭失败，则 .tar 文件可能不完整
		if er := tw.Close(); er != nil {
			err = er
		}
	}()

	// 获取文件或目录信息
	fi, er := os.Stat(srcPath)
	if er != nil {
		return er
	}

	// 获取要打包的文件或目录的所在位置和名称
	srcBase, srcRelative := path.Split(path.Clean(srcPath))

	// 开始打包
	if fi.IsDir() {
		err = cli.tarDir(srcBase, srcRelative, tw, fi)
	} else {
		err = cli.tarFile(srcBase, srcRelative, tw, fi)
	}
	if err != nil {
		return err
	}

	err = cli.Client.CopyToContainer(cli.Ctx, containerId, dstPath, &buf, types.CopyToContainerOptions{AllowOverwriteDirWithFile: true})
	if err != nil {
		return err
	}

	return nil
}

/*
	srcStr:字符
	dstPath:容器中文件
	containerId:容器ID
*/
func (cli *DockerCli) CopyToDockerByStr(srcStr, dstPath, containerId string) (err error) {
	dstBase, dstFileName := path.Split(path.Clean(dstPath))
	_, err = cli.Run(containerId, "mkdir -p "+dstBase)
	if err != nil {
		return err
	}
	var buf bytes.Buffer
	tw := tar.NewWriter(&buf)
	err = tw.WriteHeader(&tar.Header{
		Name: dstFileName,
		Mode: 0777,
		Size: int64(len(srcStr)),
	})
	if err != nil {
		return fmt.Errorf("docker copy tar: %v", err)
	}
	if _, err := tw.Write([]byte(srcStr)); err != nil {
		return fmt.Errorf("docker copy Write: %v", err)
	}
	if err := tw.Close(); err != nil {
		return fmt.Errorf("docker copy close: %v", err)
	}
	err = cli.Client.CopyToContainer(cli.Ctx, containerId, dstBase, &buf, types.CopyToContainerOptions{AllowOverwriteDirWithFile: true})
	if err != nil {
		return err
	}
	return nil
}

// 因为要执行遍历操作，所以要单独创建一个函数
func (cli *DockerCli) tarDir(srcBase, srcRelative string, tw *tar.Writer, fi os.FileInfo) (err error) {
	// 获取完整路径
	srcFull := srcBase + srcRelative

	// 在结尾添加 "/"
	last := len(srcRelative) - 1
	if srcRelative[last] != os.PathSeparator {
		srcRelative += string(os.PathSeparator)
	}

	// 获取 srcFull 下的文件或子目录列表
	fis, er := ioutil.ReadDir(srcFull)
	if er != nil {
		return er
	}

	// 开始遍历
	for _, fi := range fis {
		if fi.IsDir() {
			_ = cli.tarDir(srcBase, srcRelative+fi.Name(), tw, fi)
		} else {
			_ = cli.tarFile(srcBase, srcRelative+fi.Name(), tw, fi)
		}
	}

	// 写入目录信息
	if len(srcRelative) > 0 {
		hdr, er := tar.FileInfoHeader(fi, "")
		if er != nil {
			return er
		}
		hdr.Name = srcRelative

		if er = tw.WriteHeader(hdr); er != nil {
			return er
		}
	}

	return nil
}

// 因为要在 defer 中关闭文件，所以要单独创建一个函数
func (cli *DockerCli) tarFile(srcBase, srcRelative string, tw *tar.Writer, fi os.FileInfo) (err error) {
	// 获取完整路径
	srcFull := srcBase + srcRelative

	// 写入文件信息
	hdr, er := tar.FileInfoHeader(fi, "")
	if er != nil {
		return er
	}
	hdr.Name = srcRelative

	if er = tw.WriteHeader(hdr); er != nil {
		return er
	}

	// 打开要打包的文件，准备读取
	fr, er := os.Open(srcFull)
	if er != nil {
		return er
	}
	defer fr.Close()

	// 将文件数据写入 tw 中
	if _, er = io.Copy(tw, fr); er != nil {
		return er
	}
	return nil
}

//错误检测
//api返回的不一定正确，特定错误字符检测
func (cli *DockerCli) VerifyMessage(msg string) bool {
	if msg == "" {
		return true
	}
	//errMsg := []string{"fatal:", "error:", "command not found", "errorDetail", "ERROR", "FATAL", "failed"}
	errMsgStr := []string{`[ERROR]`}
	for _, v := range errMsgStr {
		if strings.Contains(msg, v) {
			return false
		}
	}

	//qmangoliaExitCode：\d{1}$
	errMsgRegexp := []string{`qmangoliaExitCode：\d+`, `pipeline.sh: line \d+`, `curl: \(\d\) `}
	for _, v := range errMsgRegexp {
		if b, _ := regexp.MatchString(v, msg); b {
			return false
		}
	}
	return true
}

//获取镜像名称
func (cli *DockerCli) GetImageName(Language, version string) string {
	if Language == "" || version == "" {
		return ""
	}
	imageName := ""
	if Language == "GO" {
		imageName = "golang:"
	} else if Language == "NODE" {
		imageName = "node:"
	} else if Language == "OPENJDK" {
		imageName = "openjdk:"
	} else if Language == "MAVEN" {
		imageName = "maven:"
	} else if Language == "PHP" {
		imageName = "php:"
	} else if Language == "PYTHON" {
		imageName = "python:"
	} else if Language == "CUSTOM" {

	}
	imageName += version
	return imageName
}
